Hugging Faceのモデル学習で、各レイヤ毎に別のLearning Rateで学習する方法
こんちには。
データアナリティクス事業本部 機械学習チームの中村です。
Hugging Faceのライブラリの使い方紹介記事第4弾です。
今回は、各レイヤ毎に別のLearning Rateで学習するためには、どのようにすればよいかを理解していきます。
前回のSchedulerと似た部分の内容になり、コードも一部流用しているため、併せて前回記事も参照ください。
層毎に別のLearning Rateで学習するとは
代表的な例としては、事前学習済みのBERT部分と、分類ヘッドで学習ヘッドを変更する例があります。
- 事前学習済みのBERT部分:Lerning Rateを小さく設定し、学習時に少しだけ更新する。
- 分類ヘッド:Lerning Rateを大きく設定し、学習時により大きく更新する。
今回は、この例にそって実装する方法を見ていきます。
実行環境
今回はGoogle Colaboratory環境で実行しました。
ハードウェアなどの情報は以下の通りです。
- GPU: Tesla P100 (GPUメモリ16GB搭載)
- CUDA: 11.1
- メモリ: 13GB
主なライブラリのバージョンは以下となります。
- transformers: 4.24.0
- datasets: 2.6.1
インストール
transformersとdatasetsをインストールします。
!pip install transformers datasets
また事前学習モデルの依存モジュールをインストールします。
!pip install fugashi !pip install ipadic !pip install sentencepiece
ベースとするコード
今回のベースとするコードは、前回を踏襲した以下のコードとします。
49-55行目の部分がSchedulerに関する部分でした。今回はLearning Rateをすべてのstepで一定とするconstantをSchedulerにしています。
from datasets import load_dataset from transformers import AutoTokenizer from transformers import AutoModelForSequenceClassification from transformers import TrainingArguments from transformers import Trainer from transformers import AdamW, get_constant_schedule from sklearn.metrics import accuracy_score, f1_score import torch # データセットのロード dataset = load_dataset("tyqiangz/multilingual-sentiments", "japanese") # # 実験のためデータセットを縮小したい場合はコチラを有効化 # from datasets import DatasetDict # dataset = DatasetDict({ # "train": dataset['train'].select(range(100)), # "validation": dataset['validation'].select(range(100)), # "test": dataset['test'].select(range(100)), # }) # トークナイザのロード model_ckpt = "cl-tohoku/bert-base-japanese-whole-word-masking" tokenizer = AutoTokenizer.from_pretrained(model_ckpt) # トークナイズ処理 def tokenize(batch): return tokenizer(batch["text"], padding=True, truncation=True) dataset_encoded = dataset.map(tokenize, batched=True, batch_size=None) # 事前学習モデルのロード device = torch.device("cuda" if torch.cuda.is_available() else "cpu") num_labels = 3 model = (AutoModelForSequenceClassification .from_pretrained(model_ckpt, num_labels=num_labels) .to(device)) # メトリクスの定義 def compute_metrics(pred): labels = pred.label_ids preds = pred.predictions.argmax(-1) f1 = f1_score(labels, preds, average="weighted") acc = accuracy_score(labels, preds) return {"accuracy": acc, "f1": f1} # 学習パラメータの設定 batch_size = 16 model_name = "sample-text-classification-bert" # modelから学習すべきパラメータを抽出 params = filter(lambda x: x.requires_grad, model.parameters()) # 今回はoptimizerにAdamWを使用 optimizer = AdamW(params, lr=2e-5) scheduler = get_constant_schedule(optimizer) training_args = TrainingArguments( output_dir=model_name, num_train_epochs=10, per_device_train_batch_size=batch_size, per_device_eval_batch_size=batch_size, weight_decay=0.01, evaluation_strategy="epoch", logging_strategy="steps", disable_tqdm=False, logging_steps=1, push_to_hub=False, log_level="error", ) # Trainerの定義 trainer = Trainer( model=model, args=training_args, compute_metrics=compute_metrics, train_dataset=dataset_encoded["train"], eval_dataset=dataset_encoded["validation"], tokenizer=tokenizer, optimizers=[optimizer, scheduler], ) # トレーニング実行 trainer.train()
この内容についての解説は前回の記事を参照ください。
各レイヤでLearning Rateを変更する方法
ポイントは、ベースとするコードにもある以下の部分です。
# modelから学習すべきパラメータを抽出 params = filter(lambda x: x.requires_grad, model.parameters())
ここで、今はmodel全体のパラメータを全て抽出していますが、これをレイヤごとに分けて取得できれば、異なるLearning Rateを適用することができます。
各パラメータの確認
model.parameters()
では、各パラメータの名前が分かりませんが、代わりにmodel.named_parameters()
を使用することでパラメータ名を確認できます。
list(name for name, param in model.named_parameters())
出力は以下となります。(長いため一部省略しています)
['bert.embeddings.word_embeddings.weight', 'bert.embeddings.position_embeddings.weight', 'bert.embeddings.token_type_embeddings.weight', 'bert.embeddings.LayerNorm.weight', 'bert.embeddings.LayerNorm.bias', 'bert.encoder.layer.0.attention.self.query.weight', 'bert.encoder.layer.0.attention.self.query.bias', 'bert.encoder.layer.0.attention.self.key.weight', 'bert.encoder.layer.0.attention.self.key.bias', 'bert.encoder.layer.0.attention.self.value.weight', 'bert.encoder.layer.0.attention.self.value.bias', 'bert.encoder.layer.0.attention.output.dense.weight', 'bert.encoder.layer.0.attention.output.dense.bias', 'bert.encoder.layer.0.attention.output.LayerNorm.weight', 'bert.encoder.layer.0.attention.output.LayerNorm.bias', 'bert.encoder.layer.0.intermediate.dense.weight', 'bert.encoder.layer.0.intermediate.dense.bias', 'bert.encoder.layer.0.output.dense.weight', 'bert.encoder.layer.0.output.dense.bias', 'bert.encoder.layer.0.output.LayerNorm.weight', 'bert.encoder.layer.0.output.LayerNorm.bias', 'bert.encoder.layer.1.attention.self.query.weight', 'bert.encoder.layer.1.attention.self.query.bias', 'bert.encoder.layer.1.attention.self.key.weight', 'bert.encoder.layer.1.attention.self.key.bias', 'bert.encoder.layer.1.attention.self.value.weight', 'bert.encoder.layer.1.attention.self.value.bias', 'bert.encoder.layer.1.attention.output.dense.weight', 'bert.encoder.layer.1.attention.output.dense.bias', 'bert.encoder.layer.1.attention.output.LayerNorm.weight', 'bert.encoder.layer.1.attention.output.LayerNorm.bias', 'bert.encoder.layer.1.intermediate.dense.weight', 'bert.encoder.layer.1.intermediate.dense.bias', 'bert.encoder.layer.1.output.dense.weight', 'bert.encoder.layer.1.output.dense.bias', 'bert.encoder.layer.1.output.LayerNorm.weight', 'bert.encoder.layer.1.output.LayerNorm.bias', 'bert.encoder.layer.2.attention.self.query.weight', 'bert.encoder.layer.2.attention.self.query.bias', 'bert.encoder.layer.2.attention.self.key.weight', 'bert.encoder.layer.2.attention.self.key.bias', 'bert.encoder.layer.2.attention.self.value.weight', 'bert.encoder.layer.2.attention.self.value.bias', 'bert.encoder.layer.2.attention.output.dense.weight', 'bert.encoder.layer.2.attention.output.dense.bias', 'bert.encoder.layer.2.attention.output.LayerNorm.weight', 'bert.encoder.layer.2.attention.output.LayerNorm.bias', 'bert.encoder.layer.2.intermediate.dense.weight', 'bert.encoder.layer.2.intermediate.dense.bias', 'bert.encoder.layer.2.output.dense.weight', 'bert.encoder.layer.2.output.dense.bias', 'bert.encoder.layer.2.output.LayerNorm.weight', 'bert.encoder.layer.2.output.LayerNorm.bias', ...中略... 'bert.encoder.layer.11.attention.self.query.weight', 'bert.encoder.layer.11.attention.self.query.bias', 'bert.encoder.layer.11.attention.self.key.weight', 'bert.encoder.layer.11.attention.self.key.bias', 'bert.encoder.layer.11.attention.self.value.weight', 'bert.encoder.layer.11.attention.self.value.bias', 'bert.encoder.layer.11.attention.output.dense.weight', 'bert.encoder.layer.11.attention.output.dense.bias', 'bert.encoder.layer.11.attention.output.LayerNorm.weight', 'bert.encoder.layer.11.attention.output.LayerNorm.bias', 'bert.encoder.layer.11.intermediate.dense.weight', 'bert.encoder.layer.11.intermediate.dense.bias', 'bert.encoder.layer.11.output.dense.weight', 'bert.encoder.layer.11.output.dense.bias', 'bert.encoder.layer.11.output.LayerNorm.weight', 'bert.encoder.layer.11.output.LayerNorm.bias', 'bert.pooler.dense.weight', 'bert.pooler.dense.bias', 'classifier.weight', 'classifier.bias']
簡単に説明すると、以下の部分が先頭の各種埋め込み層とLayerNormになります。
['bert.embeddings.word_embeddings.weight', 'bert.embeddings.position_embeddings.weight', 'bert.embeddings.token_type_embeddings.weight', 'bert.embeddings.LayerNorm.weight', 'bert.embeddings.LayerNorm.bias',
以下はEncoderの1層分です。baseではこれが12層あるため、layer.0~layer.11まであります。
'bert.encoder.layer.0.attention.self.query.weight', 'bert.encoder.layer.0.attention.self.query.bias', 'bert.encoder.layer.0.attention.self.key.weight', 'bert.encoder.layer.0.attention.self.key.bias', 'bert.encoder.layer.0.attention.self.value.weight', 'bert.encoder.layer.0.attention.self.value.bias', 'bert.encoder.layer.0.attention.output.dense.weight', 'bert.encoder.layer.0.attention.output.dense.bias', 'bert.encoder.layer.0.attention.output.LayerNorm.weight', 'bert.encoder.layer.0.attention.output.LayerNorm.bias', 'bert.encoder.layer.0.intermediate.dense.weight', 'bert.encoder.layer.0.intermediate.dense.bias', 'bert.encoder.layer.0.output.dense.weight', 'bert.encoder.layer.0.output.dense.bias', 'bert.encoder.layer.0.output.LayerNorm.weight', 'bert.encoder.layer.0.output.LayerNorm.bias',
以下はEncoderの最終層の先頭(CLSトークン相当部分)に線形層を通すpoolerの部分です。
'bert.pooler.dense.weight', 'bert.pooler.dense.bias',
最後に分類のためのヘッド(線形層)があります。
'classifier.weight', 'classifier.bias']
構造の詳細は、BERTの元論文やTransformerの解説をご覧ください。
設定方法
事前学習モデルの部分には"bert"という文字列、ヘッドには"classifier"という文字列で見分けて設定します。
body_params \ = [p for n,p in model.named_parameters() if n.startswith("bert") and p.requires_grad] head_params \ = [p for n,p in model.named_parameters() if n.startswith("classifier") and p.requires_grad] # 別々にLearning Rateを設定するには以下のようにする optimizer = AdamW([ {"params": body_params, 'lr': 0.0}, {"params": head_params, 'lr': 2e-5} ]) scheduler = get_constant_schedule(optimizer)
動作確認
動作確認をします。念のため、先頭と末尾のパラメータをバックアップして学習前後の変化を確認します。
from datasets import load_dataset from transformers import AutoTokenizer from transformers import AutoModelForSequenceClassification from transformers import TrainingArguments from transformers import Trainer from transformers import AdamW, get_constant_schedule from sklearn.metrics import accuracy_score, f1_score import torch # データセットのロード dataset = load_dataset("tyqiangz/multilingual-sentiments", "japanese") # # 実験のためデータセットを縮小したい場合はコチラを有効化 # from datasets import DatasetDict # dataset = DatasetDict({ # "train": dataset['train'].select(range(100)), # "validation": dataset['validation'].select(range(100)), # "test": dataset['test'].select(range(100)), # }) # トークナイザのロード model_ckpt = "cl-tohoku/bert-base-japanese-whole-word-masking" tokenizer = AutoTokenizer.from_pretrained(model_ckpt) # トークナイズ処理 def tokenize(batch): return tokenizer(batch["text"], padding=True, truncation=True) dataset_encoded = dataset.map(tokenize, batched=True, batch_size=None) # 事前学習モデルのロード device = torch.device("cuda" if torch.cuda.is_available() else "cpu") num_labels = 3 model = (AutoModelForSequenceClassification .from_pretrained(model_ckpt, num_labels=num_labels) .to(device)) backup1 = list(model.parameters())[0].to('cpu').detach().numpy().copy() backup2 = list(model.parameters())[-1].to('cpu').detach().numpy().copy() # メトリクスの定義 def compute_metrics(pred): labels = pred.label_ids preds = pred.predictions.argmax(-1) f1 = f1_score(labels, preds, average="weighted") acc = accuracy_score(labels, preds) return {"accuracy": acc, "f1": f1} # 学習パラメータの設定 batch_size = 16 model_name = "sample-text-classification-bert" # 事前学習モデルの部分には"bert"という文字列、ヘッドには"classifier"という文字列で見分けて設定 body_params = [p for n,p in model.named_parameters() if n.startswith("bert") and p.requires_grad] head_params = [p for n,p in model.named_parameters() if n.startswith("classifier") and p.requires_grad] # 別々にLearning Rateを設定するには以下のようにする optimizer = AdamW([ {"params": body_params, 'lr': 0}, {"params": head_params, 'lr': 2e-5} ]) scheduler = get_constant_schedule(optimizer) training_args = TrainingArguments( output_dir=model_name, num_train_epochs=10, per_device_train_batch_size=batch_size, per_device_eval_batch_size=batch_size, weight_decay=0.01, evaluation_strategy="epoch", logging_strategy="steps", disable_tqdm=False, logging_steps=1, push_to_hub=False, log_level="error", ) # Trainerの定義 trainer = Trainer( model=model, args=training_args, compute_metrics=compute_metrics, train_dataset=dataset_encoded["train"], eval_dataset=dataset_encoded["validation"], tokenizer=tokenizer, optimizers=[optimizer, scheduler], ) # トレーニング実行 trainer.train()
以下のように、先頭の層は値が変わらないことを確認できました。
backup1 - list(model.parameters())[0].to('cpu').detach().numpy().copy()
array([[0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], ..., [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.], [0., 0., 0., ..., 0., 0., 0.]], dtype=float32)
また以下のように、末尾の層はパラメータが更新されていることが分かります。
backup2 - list(model.parameters())[-1].to('cpu').detach().numpy().copy()
array([ 0.00139277, 0.00139004, -0.00139202], dtype=float32)
今回のように複数のLearning Rateを設定した場合、trainerのログには先頭のLearning Rateが記載されるようですので、ご注意ください。
train_log = [i for i in trainer.state.log_history if "loss" in i] train_log
[{'loss': 0.9356, 'learning_rate': 0, 'epoch': 0.14, 'step': 1}, {'loss': 0.9791, 'learning_rate': 0, 'epoch': 0.29, 'step': 2}, {'loss': 0.9827, 'learning_rate': 0, 'epoch': 0.43, 'step': 3}, {'loss': 0.9548, 'learning_rate': 0, 'epoch': 0.57, 'step': 4}, {'loss': 0.8873, 'learning_rate': 0, 'epoch': 0.71, 'step': 5}, {'loss': 0.9497, 'learning_rate': 0, 'epoch': 0.86, 'step': 6}, {'loss': 0.844, 'learning_rate': 0, 'epoch': 1.0, 'step': 7}, {'loss': 0.903, 'learning_rate': 0, 'epoch': 1.14, 'step': 8}, {'loss': 0.9116, 'learning_rate': 0, 'epoch': 1.29, 'step': 9}, {'loss': 0.8593, 'learning_rate': 0, 'epoch': 1.43, 'step': 10}, {'loss': 0.9059, 'learning_rate': 0, 'epoch': 1.57, 'step': 11}, {'loss': 0.8507, 'learning_rate': 0, 'epoch': 1.71, 'step': 12}, {'loss': 0.8737, 'learning_rate': 0, 'epoch': 1.86, 'step': 13}, ...
より進んだ応用例
より進んだ応用例としては以下も参考にされてください。weight_decayなども各層で変更している例となります。
まとめ
いかがでしたでしょうか?
実際にBERTなどのfine-tuningは、より大規模モデル(largeなど)になるにつれて学習が難しくなるタスクもあるようです。 そういった際に、Learning Rateの細かい調整が必要になるシーンがあるため記事にしてみました。
本記事がHugging Faceを使われる方の参考になれば幸いです。